package com.unisound.tracelog.client.annotation;

import com.unisound.tracelog.client.core.model.RootStartSpan;
import com.unisound.tracelog.client.core.service.TraceLogService;
import com.unisound.tracelog.client.core.support.parameter.ParameterValue;
import com.unisound.tracelog.client.core.support.parameter.annotation.IgnoreParam;
import com.unisound.tracelog.client.core.support.parameter.annotation.MethodParam;
import com.unisound.tracelog.client.core.support.parameter.annotation.ToStringParam;
import com.unisound.tracelog.common.model.span.EndSpan;
import com.unisound.tracelog.common.model.span.MultiTag;
import com.unisound.tracelog.common.model.span.StartSpanResult;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

@Aspect
public class TraceLogAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(TraceLogAspect.class);
    /**
     * 用于SpEL表达式解析.
     */
    private final ExpressionParser parser = new SpelExpressionParser();

    private TraceLogService traceLogService;

    public TraceLogAspect(TraceLogService traceLogService) {
        this.traceLogService = traceLogService;
    }

    @Pointcut("@annotation(TraceLog)")
    public void tracePointcut() {
    }

    @Around("tracePointcut() && @annotation(traceLogAnnotation)")
    public Object execute(ProceedingJoinPoint joinPoint, TraceLog traceLogAnnotation) throws Throwable {
        if (traceLogAnnotation == null) return joinPoint.proceed();
        if (!traceLogService.support()) return joinPoint.proceed();

        StartSpanResult startSpanResult = startWithoutException(joinPoint, traceLogAnnotation);

        Map<String, String> errorMessageMap = null;

        Object result = null;

        try {
            result = joinPoint.proceed();
            return result;
        } catch (Throwable e) {
            errorMessageMap = new HashMap<>();
            errorMessageMap.put("error", e.getMessage());
            throw e;
        } finally {
            endWithoutException(traceLogAnnotation.ignoreResult() ? null : result, startSpanResult, errorMessageMap);
        }
    }

    private StartSpanResult startWithoutException(ProceedingJoinPoint joinPoint, TraceLog traceLogAnnotation) {
        try {
            // 任务名称
            String name = traceLogAnnotation.value();
            if (StringUtils.isEmpty(name)) name = joinPoint.getSignature().getName();

            // 服务名称
            String serverName = traceLogAnnotation.serviceName();

            // 任务参数
            Map<String, ParameterValue> parameterMap = parameterMap(joinPoint);

            // 标签
            Map<String, String> tagMap = tagMap(parameterMap, traceLogAnnotation);

            return traceLogService.start(new RootStartSpan(name, serverName, tagMap, parameterMap));
        } catch (Throwable e) {
            LOGGER.error("TraceLog start error.", e);
            return null;
        }
    }

    private void endWithoutException(Object result, StartSpanResult startSpanResult, Map<String, String> errorMessageMap) {
        try {
            traceLogService.end(new EndSpan(startSpanResult, errorMessageMap == null ? result : errorMessageMap));
        } catch (Throwable e) {
            LOGGER.error("TraceLog end error.", e);
        }
    }

    public String generateKeyBySpringEL(String elStr, Map<String, ParameterValue> parameterMap) {
        try {
            if (parameterMap == null || parameterMap.isEmpty()) return elStr;
            if (StringUtils.isEmpty(elStr)) return elStr;
            if (!(elStr.startsWith("#") && elStr.length() > 1)) return elStr;

            EvaluationContext context = new StandardEvaluationContext();
            for (Map.Entry<String, ParameterValue> entry : parameterMap.entrySet()) {
                String paramName = entry.getKey();
                ParameterValue paramValue = entry.getValue();
                if (StringUtils.isEmpty(paramName) || paramValue == null || paramValue.getObject() == null) continue;
                context.setVariable(paramName, paramValue.getObject());
            }

            return parser.parseExpression(elStr).getValue(context).toString();
        } catch (Throwable e) {
            LOGGER.error("SpringEL绑定失败", e);
            return elStr;
        }
    }

    private Map<String, ParameterValue> parameterMap(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) return null;

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        Parameter[] parameters = method.getParameters();
        if (parameters == null || parameters.length != args.length) return null;

        Map<String, ParameterValue> parameterMap = new HashMap<>();
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            Parameter parameter = parameters[i];
            if (arg == null || parameter == null) continue;

            String parameterName = getParameterName(parameter);
            if (StringUtils.isEmpty(parameterName)) continue;

            boolean useToString = parameter.isAnnotationPresent(ToStringParam.class);
            parameterMap.put(parameterName, new ParameterValue(arg, useToString));
        }

        return parameterMap;
    }

    private String getParameterName(Parameter parameter) {
        if (parameter.isAnnotationPresent(IgnoreParam.class)) return null;

        MethodParam methodParamAnnotation = parameter.getAnnotation(MethodParam.class);
        if (methodParamAnnotation != null) {
            String name = methodParamAnnotation.value();
            if (StringUtils.isNotEmpty(name)) {
                return name;
            }
        }

        return parameter.getName();
    }

    private Map<String, String> tagMap(Map<String, ParameterValue> parameterMap, TraceLog traceLogAnnotation) {
        Map<String, String> tagMap = new HashMap<>();
        String tagKey = traceLogAnnotation.tagKey();
        String tagValue = traceLogAnnotation.tagValue();
        if (StringUtils.isNotEmpty(tagKey) && StringUtils.isNotEmpty(tagValue)) {
            String newTagValue = generateKeyBySpringEL(tagValue, parameterMap);
            if (StringUtils.isNotEmpty(newTagValue)) {
                tagMap.put(tagKey, newTagValue);
            }
        }

        if (parameterMap != null) {
            for (Map.Entry<String, ParameterValue> entry : parameterMap.entrySet()) {
                ParameterValue parameterValue = entry.getValue();
                if (parameterValue != null) {
                    Object object = parameterValue.getObject();
                    if (object != null && object instanceof MultiTag) {
                        MultiTag multiTag = (MultiTag) object;
                        Map<String, String> map = multiTag.tagMap();
                        if (map != null) {
                            tagMap.putAll(map);
                        }
                    }
                }
            }
        }

        return tagMap;
    }

}
